package server;

import java.sql.SQLException;

import server.Log.directOpt;

//an exception here might invalidate the state.

/**
 * Responsible for translating the client's commands into Database and FireWall commands, while supporting error and rollback handling.
 */
public class Commands {
	// if you change this, change in core/parser.c and core/ipt_ipcontext.h as well
	protected static final int MAX_REGEXP_SIZE = 250;

	protected static final int MAX_CONTEXT_RULES = 8;
	protected static final int MAX_RULES = 10;

	/**
	 * Initializes the Commands class using the given ConfigurationManager.
	 *
	 * @param config    The ConfigurationManager to use
	 *
	 * @throw SQLException on SQL error
	 * @throw Exception    on any other error
	 */
	public Commands(ConfigurationManager config) throws SQLException, Exception
	{
		this.config = config;
		mysql = new MYSQLComm(
				config.getDBIP(),
				config.getDBName(),
				config.getDBUser(),
				config.getDBPassword(),
				true);
		addGlobalLog("Firewall Command Manager started");
	}

	/**
	 * Checks if the firewall is on.
	 * 
	 * @return true - the firewall is on, false - the firewall isn't
	 *  
	 * @throws FirewallException on any error.
	 */
	public synchronized boolean isOn()
	{
		return config.getIsFirewallOn();
	}

	/**
	 * Turns on the firewall.
	 *
	 * @throw FirewallException on any error
	 */
	public synchronized void turnOn() throws FirewallException
	{
		Boolean old = isOn();
		try {
			config.setIsFirewallOn(true);
			switchRuleSet(getCurrentRuleSet());
			addGlobalLog("Firewall turned on");
		} catch (FirewallException e) {
			addGlobalLog("Failed turning on firewall: " + e.getMessage());
			config.setIsFirewallOn(old);
			throw e;
		}
	}

	/**
	 * Turns off the firewall.
	 *
	 * @throw FirewallException on any error
	 */
	public synchronized void turnOff() throws FirewallException
	{
		try {
			iptables.flush();
			config.setIsFirewallOn(false);
			addGlobalLog("Firewall turned off");
		} catch (FirewallException e) {
			addGlobalLog("Failed turning off firewall: " + e.getMessage());
			throw e;
		}
	}

	/// @return 'true' if log enabled, 'false' if not.
	public synchronized boolean isLogEnabled()
	{
		return config.getIsLogActive();
	}

	/**
	 * Enables logging for all active rules.
	 * Unloads all rules, then reloads them with logging enabled.
	 *
	 * @throw FirewallException on any error
	 */
	public synchronized void enableLog() throws FirewallException
	{
		boolean old = isLogEnabled();
		try {
			config.setIsLogActive(true);
			switchRuleSet(getCurrentRuleSet());
			addGlobalLog("Logging turned on");
		} catch (FirewallException e) {
			addGlobalLog("Failed enabling log: " + e.getMessage());
			config.setIsLogActive(old);
			throw e;
		}
	}

	/**
	 * Disables logging for all active rules.
	 * Unloads all rules, then reloads them with logging disabled.
	 *
	 * @throw FirewallException on any error
	 */
	public synchronized void disableLog() throws FirewallException
	{
		boolean old = isLogEnabled();
		try {
			config.setIsLogActive(false);
			switchRuleSet(getCurrentRuleSet());
			addGlobalLog("Disabled log");
		} catch (FirewallException e) {
			addGlobalLog("Failed disabling log: " + e.getMessage());
			config.setIsLogActive(old);
			throw e;
		}
	}

	/**
	 * Checks if the provided username and password match, and returns the
	 * user's info if they do.
	 *
	 * @param   username The user's name
	 * @param   password The user's password
	 *
	 * @throw InvalidLoginException If username doesn't match password, or if
	 *                               the user doesn't exist
	 * @throw FirewallException On any other error
	 */
	public synchronized Password checkUser(String username, String password) throws InvalidLoginException, FirewallException
	{
		try {
			Password info = mysql.getPassword(username);
			if (info.password.compareTo(password) != 0) {
				addGlobalLog("Invalid login attempt for " + username);
				throw new InvalidLoginException();
			}
			addGlobalLog(username + " logged in");
			return info;
		} catch (SQLException e) {
			if (e.getMessage().compareTo("No such user") == 0) {
				addGlobalLog("Invalid login - no such user " + username);
				throw new InvalidLoginException();
			} else {
				addGlobalLog("Error logging in: " + e.getMessage() + " (" + e.getErrorCode() +")");
				throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
			}
		}
	}

	/**
	 * Adds the given rule to the specified ruleset, in the given direction.
	 *
	 * @param   dir     Direction to use
	 * @param   ruleset Ruleset to use
	 * @param   rule    The rule to add
	 *
	 * @throw FirewallException on any error
	 */
	public synchronized void addRule(RulesTableType dir, String ruleset, Rule rule) throws FirewallException
	{
		addRule(dir, ruleset, rule, true);
		if (isLogEnabled())
			switchRuleSet(getCurrentRuleSet());
	}

	// do rollback & commit support in SQL. implment this that way.
	/**
	 * Deletes the rule by the given index, in the specified 
	 * ruleset and direction.
	 *
	 * @param   dir     Direction
	 * @param   ruleset Ruleset to use
	 * @param   index   Rule index to delete
	 * 
	 * @throw  FirewallException   on any error
	 */
	public synchronized void deleteRule(RulesTableType dir, String ruleset, int index) throws FirewallException
	{
		try {
			mysql.deleteRule(getTableName(dir, ruleset), index);
			if (!isLogEnabled() && getCurrentRuleSet().compareTo(ruleset) == 0 && isOn())
				iptables.remove(dir, index);
			mysql.deleteRuleComplete(true);
			addGlobalLog("Deleted rule " + index + ", direction " + 
					((dir == RulesTableType.IN) ? "in" : "out"));
		} catch (SQLException e) {
			try {
				mysql.deleteRuleComplete(false);
			} catch (SQLException ex) {
				addGlobalLog("Failed deleting rule: " + e.getMessage() + " (" + e.getErrorCode() +")");
				throw new FirewallException(ex.getMessage() + " (" + e.getErrorCode() +")");
			}
			addGlobalLog("Failed deleting rule: " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}
		if (isLogEnabled() && getCurrentRuleSet().compareTo(ruleset) == 0)
			switchRuleSet(getCurrentRuleSet());
	}

	/**
	 * Retrieves all rules in the given ruleset.
	 *
	 * @param   dir     Direction to use
	 * @param   ruleset Ruleset to retrieve rules from
	 *
	 * @return  Array of all rules in ruleset
	 *
	 * @throw FirewallException    on any error
	 */
	public synchronized Rule[] getRules(RulesTableType dir, String ruleset) throws FirewallException
	{
		try {
			return mysql.getRules(getTableName(dir, ruleset));
		} catch (SQLException e) {
			addGlobalLog("Failed retrieving rules: " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}		
	}

	/**
	 * Adds the given protocol to the protocols list
	 *
	 * @param   protocol    protocol to add
	 *
	 * @throw FirewallException    on any error
	 */
	public synchronized void addProtocol(Protocol protocol) throws FirewallException
	{
		try {
			if (protocol.outReg != null && protocol.outReg.length() > MAX_REGEXP_SIZE) {
				throw new FirewallException("Out regexp too long. Max " + MAX_REGEXP_SIZE);
			}
			if (protocol.inReg != null && protocol.inReg.length() > MAX_REGEXP_SIZE) {
				throw new FirewallException("In regexp too long. Max " + MAX_REGEXP_SIZE);
			}
			mysql.addProtocol(protocol);
			addGlobalLog("Added protocol definition for " + protocol.protocol);
		} catch (SQLException e) {
			addGlobalLog("Failed adding protocol " + protocol.protocol + 
					": " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}	
	}

	/**
	 * Deletes the given protocol from the protocols list
	 *
	 * @param   name    name of protocol to delete
	 *
	 * @throw FirewallException    on any error
	 */
	public synchronized void deleteProtocol(String name) throws FirewallException
	{
		try {
			mysql.deleteProtocol(name);
			addGlobalLog("Deleted protocol definition for " + name);
		} catch (SQLException e) {
			addGlobalLog("Failed deleting protocol " + name + 
					": " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}
	}

	/**
	 * Updates the given protocol to the protocols list
	 *
	 * @param   protocol    protocol to update
	 *
	 * @throw FirewallException    on any error
	 */
	public synchronized void updateProtocol(Protocol protocol) throws FirewallException
	{
		try {
			mysql.updateProtocol(protocol);
			addGlobalLog("Updated protocol definition for " + protocol.protocol);
		} catch (SQLException e) {
			addGlobalLog("Failed updating protocol " + protocol.protocol + 
					": " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}	
	}

	/**
	 * Retrieves all protocols defined in the system.
	 *
	 * @return  Array of all defined protocols.
	 *
	 * @throw FirewallException    on any error
	 */
	public synchronized Protocol[] getProtocols() throws FirewallException
	{
		try {
			return mysql.getProtocols();
		} catch (SQLException e) {
			addGlobalLog("Failed retrieving protocols: " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}		
	}

	/**
	 * Creates a new ruleset, containing all the currently active rules.
	 *
	 * @param   name    Name of new ruleset to create.
	 *
	 * @throw FirewallException    on any error
	 */
	public synchronized void createRuleSet(String name) throws FirewallException
	{
		if (name.compareToIgnoreCase("Default") == 0)
			throw new FirewallException("Can't create default ruleset.");
		try {
			mysql.createRuleSet(name);
			addGlobalLog("Ruleset " + name + " created");
		} catch (SQLException e) {
			if (e.getErrorCode() == 1050)
			{
				addGlobalLog("Ruleset " + name + " already exists");
				throw new FirewallException("Ruleset " + name + " already exists");				
			}
			addGlobalLog("Failed creating ruleset " + name + 
					": " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}	
	}

	/**
	 * Deletes a given ruleset from the database.
	 * Don't do this while it's the active ruleset...
	 *
	 * @param   name    Name of ruleset to delete.
	 *
	 * @throw FirewallException    on any error
	 */
	public synchronized void deleteRuleSet(String name) throws FirewallException
	{
		if (name.compareToIgnoreCase("Default") == 0)
			throw new FirewallException("Can't delete default ruleset.");
		try {
			mysql.deleteRuleSet(name);
			addGlobalLog("Ruleset " + name + " deleted");
		} catch (SQLException e) {
			addGlobalLog("Failed deleting ruleset " + name + 
					": " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}
	}

	/**
	 * Retrieves all rulesets in the system.
	 *
	 * @return  Array of all rulesets in the system.
	 *
	 * @throw FirewallException    on any error
	 */	
	public synchronized String[] getRuleSets() throws FirewallException
	{
		try {
			String[] ruleSets = mysql.getRuleSets();
			String[] ruleSetsDefault = new String[ruleSets.length+1];
			System.arraycopy(ruleSets, 0, ruleSetsDefault, 0, ruleSets.length);
			ruleSetsDefault[ruleSets.length] = "Default";
			return ruleSetsDefault;
		} catch (SQLException e) {
			addGlobalLog("Failed retreiving rulesets: " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}	
	}

	/**
	 * Sets a new active ruleset. All rules in this rulesets are applied.
	 *
	 * @param   name    Name of ruleset to activate
	 *
	 * @throw FirewallException    on any error
	 */	
	public synchronized void setRuleSet(String name) throws FirewallException
	{
		String old = getCurrentRuleSet();
		try {
			config.setActiveRuleset(name);
			switchRuleSet(name);
		} catch (FirewallException e) {
			// error - try to undo
			addGlobalLog("Failed setting ruleset to " + name + 
					": " + e.getMessage());
			config.setActiveRuleset(old);
			switchRuleSet(old);
			throw e;
		}
		addGlobalLog("Set ruleset to " + name);
	}

	/// @return name of active ruleset
	public synchronized String getCurrentRuleSet()
	{
		return config.getActiveRuleset();
	}

	/**
	 * Retrieves log messages from the specified time period, by IP address,
	 * port, and direction.
	 *
	 * @param   days    Log messages will be retrieved this many days back
	 * @param   address IP range to use (can be null, to use any address)
	 * @param   port    Port to use
	 * @param   dir     Direction to use
	 *
	 * @return  Array of matching log messages
	 *
	 * @throw FirewallException    on any error
	 */	
	public synchronized Log[] getLogs(Integer days, Log.typeOpt type, String protocol, String ip, Integer port, directOpt dir) throws FirewallException
	{
		try {
			return mysql.getLogs(days, type, protocol, ip, port, dir);
		} catch (SQLException e) {
			addGlobalLog("Failed getting logs: " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}
	}

	/**
	 * Retrieves global log messages from the specified time period.
	 *
	 * @param   days        Log messages will be retrieved this many days back
	 *
	 * @return  Array of matching log messages
	 *
	 * @throw FirewallException    on any error
	 */	
	public synchronized GlobalLog[] getGlobalLogs(int days) throws FirewallException
	{
		try {
			return mysql.getGlobalLogs(days);
		} catch (SQLException e) {
			addGlobalLog("Failed getting global logs: " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}	
	}

	/**
	 * Stores a new global log message.
	 *
	 * @param   msg Message to log
	 *
	 * @throw FirewallException on any error
	 */   
	public synchronized void addGlobalLog(String msg) throws FirewallException
	{
		try {
			System.out.println(msg);
			mysql.addGlobalLog(msg);
		} catch (SQLException e) {
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		}	
	}

	private MYSQLComm mysql;
	private ConfigurationManager config;
	private IPTablesComm iptables = new IPTablesComm();

	/** Package ctor for testing */
	Commands(ConfigurationManager config, 
			MYSQLComm sqlComm, 
			IPTablesComm iptablesComm)
			{
		this.config = config;
		this.mysql = sqlComm;
		this.iptables = iptablesComm;
			}

	/**
	 * Switches the active ruleset to the one given.
	 * 
	 * @param name The name of the new ruleset
	 * 
	 * @throws FirewallException on any error
	 */
	private void switchRuleSet(String name) throws FirewallException
	{
		try {
			Rule[] InRules = mysql.getRules(getTableName(RulesTableType.IN, name));
			Rule[] OutRules = mysql.getRules(getTableName(RulesTableType.OUT, name));
			if (isOn())
			{
				// We use our own rule indexing here because if log is on then each
				// IP rule must be translated to *two* IPTables rules: one for log
				// and one for filtering.
				// Context rules are always a single IPTables rule.
				int index = 1;
				boolean logOn = isLogEnabled();
				iptables.flush();
				for (Rule rule : InRules) {
					int origIndex = rule.index;

					rule.index = index;
					addRule(RulesTableType.IN, name, rule, false);
					if ((logOn) && (rule.type == Rule.typeOpt.IP)) {
						index += 2;
					} else {
						index ++;
					}

					rule.index = origIndex;
				}

				index = 1;
				for (Rule rule : OutRules) {
					int origIndex = rule.index;

					rule.index = index;
					addRule(RulesTableType.OUT, name, rule, false);
					if ((logOn) && (rule.type == Rule.typeOpt.IP)) {
						index += 2;
					} else {
						index ++;
					}

					rule.index = origIndex;
				}
				addGlobalLog("Switched ruleset to " + name);
			}
		} catch (SQLException e) {
			addGlobalLog("Failed switching ruleset to " + name + 
					": " + e.getMessage() + " (" + e.getErrorCode() +")");
			throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
		} catch (FirewallException e) {
			addGlobalLog("Failed switching ruleset to " + name + 
					": " + e.getMessage());
			throw e;
		}
	}

	/**
	 * Constructs the table name for a given ruleset and direction. 
	 * 
	 * @param dir The direction of the ruleset
	 * @param ruleset The ruleset name
	 * 
	 * @return The table name for the given ruleset and direction
	 */
	private String getTableName(RulesTableType dir, String ruleset)
	{
		String ret;
		if (ruleset.compareToIgnoreCase("default") == 0)
			ret = "ActiveRules_";
		else
			ret = "RuleSet_" + ruleset + "_";
		ret += (dir == RulesTableType.IN) ? "In" : "Out";	
		return ret;
	}
	
	/// @return number of context rules for given ruleset & direction
	private int getContextRulesNum(RulesTableType dir, String ruleset) throws FirewallException
	{
		int result = 0;
		Rule[] rules = getRules(dir, ruleset);
		for (Rule rule : rules)
			if (rule.type == Rule.typeOpt.CONTEXT)
				result ++;
		return result;
	}

	/// @return total number of rules (context & IP) for given ruleset & direction
	private int getRulesNum(RulesTableType dir, String ruleset) throws FirewallException
	{
		Rule[] rules = getRules(dir, ruleset);
		return rules.length;
	}

	private synchronized void addRule(RulesTableType dir, String ruleset, Rule rule, boolean updatedb) throws FirewallException
	{
		Protocol protocol;
		String regexp = null;
		if (rule.type == Rule.typeOpt.CONTEXT)
		{
			addGlobalLog(
					"Adding context rule " + rule.index + 
					" port " + rule.port + 
					" protocol " + rule.protocol + 
					" direction " + ((dir == RulesTableType.IN) ? "in" : "out")
			);

			// Too many context rules already?
			// Do this check only if we're actually adding a rule to the DB, and not
			// updating iptables
			if (updatedb && getContextRulesNum(dir, ruleset) >= MAX_CONTEXT_RULES) {
				addGlobalLog("Too many context rules. Max " + MAX_CONTEXT_RULES);
				throw new FirewallException
				("Too many context rules. Max " + MAX_CONTEXT_RULES);
			}

			try {
				protocol = mysql.getProtocol(rule.protocol);
			} catch (SQLException e) {
				addGlobalLog("Failed adding rule: " + e.getMessage() + " (" + e.getErrorCode() +")");
				throw new FirewallException(e.getMessage() + " (" + e.getErrorCode() +")");
			}
			if (dir == RulesTableType.IN)
				regexp = protocol.inReg;
			else
				regexp = protocol.outReg;
			if (regexp == null)
				throw new FirewallException("No regexp defined for this direction in the protocol.");
		} else {
			addGlobalLog(
					"Adding IP rule " + rule.index + 
					" port " + rule.port + 
					" IP range " + rule.iprange + 
					" direction " + ((dir == RulesTableType.IN) ? "in" : "out")
			);
		}

		// Check total number of rules limit
		// Do this check only if we're actually adding a rule to the DB, and not
		// updating iptables
		if (updatedb && getRulesNum(dir, ruleset) >= MAX_RULES) {
			addGlobalLog("Too many rules. Max " + MAX_RULES);
			throw new FirewallException("Too many rules. Max " + MAX_RULES);
		}

		// Add rule to DB
		if (updatedb)
		{
			try {
				mysql.addRule(getTableName(dir, ruleset), rule);
			} catch (SQLException e) {
				addGlobalLog("Failed adding rule: " + e.getMessage());
				throw new FirewallException(e.getMessage());
			}
		}

		if (getCurrentRuleSet().compareTo(ruleset) == 0 && isOn())
		{
			// And now to IPTables
			try {
				iptables.add(dir, rule, isLogEnabled(), regexp);
			} catch (FirewallException e) {
				addGlobalLog("Failed adding rule: " + e.getMessage());
				if (updatedb)
				{
					try {
						mysql.deleteRule(getTableName(dir, ruleset), rule.index);
						mysql.deleteRuleComplete(true);
					} catch (SQLException ex) {
						try {
							mysql.deleteRuleComplete(false);
						} catch (SQLException ex1) {
							throw new FirewallException(ex1.getMessage() + " (" + ex1.getErrorCode() +")");					
						}
						throw new FirewallException(ex.getMessage() + " (" + ex.getErrorCode() +")");
					}
				}
				throw e;
			}
		}
		addGlobalLog("Rule added successfully");
	}

}
